This document is a efford to introduce the strengths and benefits of functional programming in scala.
We do not claim intellectual property of all the material presented. We specifically refer to the original resources whenever is needed.
The presentation path of the concepts is still under consideration and may be changed in future reviews.
resource: Scala IO 2013, Purely functional I/O in Scala, R.O. Bjarnason
A pure function of type A => B
takes an argument of type A
and returns a value of type B
.
And does nothing else
A pure function always returns the same value given the same arguments.
A pure function has no dependencies other than its arguments.
The result of calling a pure function can be understood completely by looking at the returned value.
In [ ]:
// This is how we define method in scala
def inc(x: Int): Int = { x + 1 }
//This is how we define an (anonymous) function
(x: Int) => { x + 1 }
//We can give a name to an anonymous function by assigning it to a variable
val incAnon: Int => Int = (x: Int) => { x + 1 }
// In scala methods are not exactly the same with functions
// We can convert a method to a funciton via an `_`
inc _
def sample(f: Int => Int, x :Int): Int = {
f(x)
}
// For the purposes of this presentation we will ignore this technicality
In [ ]:
//Let's define some pure functions (methods)
def inc(x: Int): Int = x + 1
def double = (x: Int) => x * 2 // We can omit result type (= scala local type inference)
def stringify (x: Int): String = x.toString()
In [ ]:
// The functions below are impure - why?
def printInt(x: Int): Unit = {
println(x)
}
def divideTenBy(x: Double): Double = 10 / x
def allowOnlyPositive(x: Int):Int = if (x > 0) x else sys.error("only positive numbers")
We can call evaluate/execute without fear the pure functions but what about the impure ones?
In [ ]:
inc(1)
double(2)
stringify(2)
//With impure functions but things happen
divideTenBy(2)
divideTenBy(0) // Not ok!
Impure functions are not so cool...
How would you test function printInt?
The call of divideTenBy(0) gives infinity (???).
Also the call of allowOnlyPositive(0) causes a RuntimeException.
In [ ]:
allowOnlyPositive(-1) // Not ok!
In [ ]:
//((A => B), (B => C)) => (A => C) :: andThen andThen((inc _),(allowOnlyPositive))
// inc o double
val incAfterDouble = (inc _).compose(double) // inc(double(x))
def s2(x:Int) = inc(double(x))
s2(10)
val incAndThenAllowOnlyPositive = inc _ andThen allowOnlyPositive
incAfterDouble(10)
// incAndThenAllowOnlyPositive(-2)
So far so good, function purity is desired in programs, but the reality is devastating. All the programs that matter to us "contain" side effects.
So is this introduction all in vain?
Purity matters even if the overall execution of our programs is impure. What we actually desire is to separate the impure parts of our programs from the pure ones. That means that we write the "business logic" or "logic" of our program in pure functions and hand the results to impure functions to do the "dirty work". In our context the "dirty work" is side effects of all kinds.
Resource: Functional programming in scala: Chapter 13
Consider fragment of code below which calculates the winner of a contest:
In [ ]:
// Program #1
// Decide the winner of a contest.
case class Player(name: String, score: Int)
def contest(p1: Player, p2: Player): Unit = {
if(p1.score > p2.score)
println(s"${p1.name} wins!")
else if(p2.score > p1.score)
println(s"${p2.name} wins!")
else
println("It is a draw!")
}
// Who is the winner?
val p1 = Player("fpas", 10)
val p2 = Player("gsmyrn", 10)
contest(p1,p2)
We are done right? The program works and prints the correct results.
This program, though simple, has some flows. One of them is that it cannot be tested (easily). This is because the the actual "logic" of computing the winner is interleaved with the mechanism of printing the result.
Let's change this.
In [ ]:
// Program #2
// Split the logic from the effect.
case class Player(name: String, score: Int)
def computeWinner(p1: Player, p2: Player): Player = {
if(p1.score > p2.score) p1
else p2
}
def displayWinner(p: Player): Unit = {
println(s"${p.name} wins!")
}
//This is function composition
def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))
// Who is the winner?
contest(Player("fpas", 10), Player("gsmyrn", 5))
With this simple refactor the contest function is a composition of two other functions. The first part is a pure function containing the logic and the second part is an impure function that dispays the results.
The overall contest
function is impure.
Take away 1
Given an impure function of type
f: A => B
we can split into to other functions.
- A pure function of type
A => D
whereD
describes the result off
.- An impure function of type
D => B
, which interpretsD
and executes the result.
That is what we strive to do in functional programming. Describe all the operations of a program in composed pure functions and promote/push/delay the (side) effects execution to the end of the "chain".
The carefull reader, by now, will have discover an incosistency between Program 1
and Program 2
. The wo programs are not equivalent.
What happened to the draw case in Program 2
?
This was intentional and the inconsistency is caused by the fact that our pure function computeWinner
cannot handle the case of a draw.
It cannot handle it because there is no obvious result of type Player
to return in the case of scores equality.
So a pure function such as computeWinner
cannot handle with the cases where there is no winner.
This is natural because a pure function must define an output for each given input.
But in our case we want the function to handle cases of player pairs where there is no output (winner).
In [ ]:
// Program #3
// A better computeWinner
def computeWinner(p1: Player, p2: Player): Player = {
if(p1.score > p2.score) p1
else if (p2.score > p1.score) p2
else null //null is a special value for this function that denotes that there is no winner.
}
Problem solved ! The function remains pure ( null
is value of type Player
). Let's try it !
In [ ]:
//[Program 3 - continue
def displayWinner(p: Player): Unit = {
println(s"${p.name} wins!")
}
//This is function composition
def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))
// Who is the winner?
contest(Player("fpas", 10), Player("gsmyrn", 10))
Not what we expected, a NullPointerException
. This happens because displayWinner
must learn to handle null
values of players.
We can do that!
But wait, do we want to do that? Is there a better way?
We don't want to handle null cases in our code like that. null leads to hidden side effects like exceptions.
We want the end user of the pure function
computeWinner
to have a good description type for its result.
So the correct question is:
Which data type is appropriate to describe the side effect of partial functions?
An alternative more functional approach to the "partial function" side effect problem is the Option[A]
type.
In [ ]:
// Program #4
// computeWinner with Option[A]
case class Player(name: String, score: Int)
def computeWinner(p1: Player, p2: Player): Option[Player] = {
if(p1.score > p2.score) Some(p1) //:Option[Player]
else if (p2.score > p1.score) Some(p2)
else None //: Option[Player]
}
def displayWinner(op: Option[Player]): Unit = {
if(op.isEmpty)
println(s"It's a draw")
else
println(s"${op.get.name} wins!")
}
// This is the alternative with pattern matching
// def displayWinner(p: Option[Player]): Unit = p match { //Pattern match instead of 'if' construct
// case Some(x) => println(s"${x.name} wins!")
// case None => println(s"It's a draw")
// }
//This is function composition
def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))
// Who is the winner? (handles draw)
contest(Player("fpas", 5), Player("gsmyrn", 5))
Program 4
implementation is real close to Program 3
implementation which used null
in place of None
.
The key difference is that according to Take away 1 we realized that the pure function
f: A =>D
(computeWinner
) used a data typeD
(Player
) that was not expressive enough to describe the side effect of "partial function"
So we chose an "embelised type" to describe the absense of return value at some cases.
Note, that now every one that reads the signature
def computeWinner(p1: Player, p2: Player): Option[Player]
knows that the method does something that may or may not return a value. The side effect is described and is not hidden as in the case of null
usage, or any other programming convention.
Exercise 1: Rewrite contest
function using andThen
composition function.
Hint use Function1.curried
method
In [ ]:
//This is function composition
// def contest(p1: Player, p2: Player) = displayWinner(computeWinner(p1,p2))
def contest(p1: Player, p2:Player) = ???
contest(Player("fpas", 5), Player("gsmyrn", 5))
Exercise 2: There is a new requirement to CAPITALIZE the names of the winners.
a) Create a new method that capitalize
a player name.
b) Compose in a functional way the new function into the program.
Hint use option map
method.
In [ ]:
// Exercise #2
case class Player(name: String, score: Int)
def computeWinner(p1: Player, p2: Player): Option[Player] = {
if(p1.score > p2.score) Some(p1)
else if (p2.score > p1.score) Some(p2)
else None
}
//Player => Player
def capitalize(p: Player): Player = Player(p.name.toUpperCase, p.score)
def displayWinner(p: Option[Player]): Unit = p match {
case Some(p) => println(s"${p.name} wins!")
case None => println(s"It's a draw")
}
//This is function composition
def contest(p1: Player, p2: Player): Unit = displayWinner(computeWinner(p1,p2).map(capitalize))
contest(Player("fpas", 10), Player("gsmyrn", 10))
Exercise 3: There is a new requirement
If the winner of a contest has a negative or zero score, then the result should be 'a draw'.
Implement a method that checks the winners score.
Hint use option flatMap
method
In [ ]:
// Exercise 3
case class Player(name: String, score: Int)
def computeWinner(p1: Player, p2: Player): Option[Player] = {
if(p1.score > p2.score) Some(p1)
else if (p2.score > p1.score) Some(p2)
else None
}
def capitalize(p: Player): Player = p.copy(name = p.name.toUpperCase)
def checkScore(p: Player): Option[Player] = ???
def displayWinner(p: Option[Player]): Unit = p match {
case Some(p) => println(s"${p.name} wins!")
case None => println(s"It's a draw")
}
//This is function composition
def contest(p1: Player, p2: Player) = ???
contest(Player("fpas", 0), Player("gsmyrn",-15))
Scala is an expressive language that is able to support functional programming paradigm.
In functional programming paradigm we try to describe with higher constructs the execution of programs.
We also try to handle side effects using contained and controlled parts of the program that interpret how to execute the side effects.
Side effects are described in functional programming using more expressive types ("embelished types").
Functional programming tries to promote Composability as a mean to achieve all the above goals.
Finally, functional programming requires a paradigm shift
FROM writing programs that do operations TO writing programs that describe oparations
Fotios Paschos, @fpaschos
, Sep 2017